import { Page, SerializableOrJSHandle } from "puppeteer-core";
import { Extractor } from "../class/Extractor";
import { Spider } from "../class/Spider";

/**
 * 使用选择器选择单个元素。
 * @param selector 选择器
 */
export function $(selector: string): PropertyDecorator {
    return function (target: Object, propertyKey: string | symbol) {
        async function handle(this: Extractor, page: Page) {
            const value = await page.$(selector);
            Object.defineProperty(this, propertyKey, { value });
        }
        addHandle(target, handle)
    }
}
/**
 * 使用选择器选择多个元素。
 * @param selector 选择器
 * @returns 
 */
export function $$(selector: string): PropertyDecorator {
    return function (target: Object, propertyKey: string | symbol) {
        async function handle(this: Extractor, page: Page) {
            const value = await page.$$(selector);
            value.length > 0 ? Object.defineProperty(this, propertyKey, { value }) : null;
        }
        addHandle(target, handle)
    }
}

/**
 * 对单个元素执行一段代码(代码在浏览器里执行)
 * @param selector 选择器
 * @returns 
 */
export function $eval(selector: string, handle: (element: Element, ...args: unknown[]) => any, ...args: SerializableOrJSHandle[]): PropertyDecorator {
    return function (target: Object, propertyKey: string | symbol) {
        async function _handle(this: Extractor, page: Page) {
            const value = await page.$eval(selector, handle, ...args);
            Object.defineProperty(this, propertyKey, { value });
        }
        addHandle(target, _handle)
    }
}

/**
 * 对多个元素执行一段代码(代码在浏览器里执行)
 * @param selector 选择器
 * @param handle 处理器
 * @param args 处理器的参数，这些参数可以传如到浏览器里的handle函数里，解决浏览器的函数不能调用node里的变量问题
 * @returns 
 */
export function $$eval(selector: string, handle: (elements: Element[], ...args: unknown[]) => any, ...args: SerializableOrJSHandle[]): PropertyDecorator {
    return function (target: Object, propertyKey: string | symbol) {
        async function _handle(this: Extractor, page: Page) {
            const value = await page.$$eval(selector, handle, ...args);
            Object.defineProperty(this, propertyKey, { value });
        }
        addHandle(target, _handle)
    }
}

/**
 * 获取单个元素的内容
 * @param selector 选择器
 * @returns 
 */
export function $text(selector: string) {
    return $eval(selector, element => element.textContent?.trim());
}

/**
 * 获取多个元素的内容
 * @param selector 选择器
 * @returns 
 */
export function $$text(selector: string) {
    return $$eval(selector, elements => elements.map(element => element.textContent?.trim()));
}

/**
 * 获取单个元素的HTML
 * @param selector 选择器
 * @param inner 执行 innerHtml 还是 outerHtml
 * @returns 
 */
export function $html(selector: string, inner: boolean = true) {
    return $eval(selector, (element, inner) => inner ? element.innerHTML.trim() : element.outerHTML.trim(), inner);
}
/**
 * 获取多个元素的HTML
 * @param selector 选择器
 * @param inner 执行 innerHtml 还是 outerHtml
 * @returns 
 */
export function $$html(selector: string, inner: boolean = true) {
    return $$eval(selector, (elements, inner) => elements.map(element => inner ? element.innerHTML.trim() : element.outerHTML.trim()), inner);
}

/**
 * 获取单个元素的属性值
 * @param selector 选择器
 * @param attr 属性明，默认是 href
 * @returns 
 */
export function $attr(selector: string, attr: string = "href") {
    return $eval(selector, (element, attr) => element.getAttribute(attr as string), attr);
}
/**
 * 获取多个元素的属性值
 * @param selector 选择器
 * @param attr 属性明，默认是 href
 * @returns 
 */
export function $$attr(selector: string, attr: string = "href") {
    return $$eval(selector, (element, attr) => element.map(element => element.getAttribute(attr as string)), attr);
}

/**
 * 通过名字获取一个 spider
 * @param name spider 的名字
 * @returns 
 */
export function spider(name: string): PropertyDecorator {
    return function (target: Object, propertyKey: string | symbol) {
        async function handle(this: Extractor) {
            const value = Spider.getSpider(name);
            Object.defineProperty(this, propertyKey, { value });
        }

        addHandle(target, handle);
    }
}

/**
 * 像当前类添加静态属性 handles
 * 直接使用继承的 handles 会导致所有的 Extractor 使用相同的
 * 使用 Object.defineProperty 定义可以解决问题
 * @param obj 
 * @param handle 
 */
function addHandle(obj: Object, handle: (page: Page) => Promise<void>) {
    if (!(Object.getOwnPropertyDescriptor(obj.constructor, "handles")?.value instanceof Array)) {
        Object.defineProperty(obj.constructor, "handles", {
            value: []
        })
    }
    const handles = Object.getOwnPropertyDescriptor(obj.constructor, "handles")?.value as Function[]
    handles.push(handle)
}


